Skip to content

Latest commit

 

History

History
1751 lines (1741 loc) · 71.3 KB

Android Fragments Tutorial An Introduction with Kotlin.md

File metadata and controls

1751 lines (1741 loc) · 71.3 KB

Android Fragment教程:介绍 - Kotlin


更新日志 :本教程已由Lance Gleason更新至支持Kotlin及Android Studio 3.0的版本。原教材是由Huyen Tue Dao编写的。

Fragments-feature 在现代的Android app开发中,fragment是构成UI布局非常的常用工具。在本教程中你会深入Android Fragment的基础概念,并创建一个app来展示暴走漫画。

fragment | noun | /’frag-mənt/
孤立的或未完成的部分

fragment是一个Android的组件,用来管理activity中的一部分UI。正如它的名字所体现的,fragment并非是一个独立的实体,而是寄存在某个activity下。

在很多方面,fragment都有着类似于activity的特性。

想象一下,你是一个activity,你有很多事需要做,于是就雇佣了很多迷你的自己来运行,用洗衣和交税来换取住宿和食物。这就很像是activity和fragment之间的关系。

现在,可能就像实践上你并不需要几个听从命令的部下,你也并不一定需要使用fragment。然而,如果你可以很好地使用fragment,它就可以为你提供:

  • 模块性:将复杂的activity代码拆分为一个个的fragment,以获得更好的组织性和可维护性。
  • 可复用性:将行为或UI部分放置到多个fragment中,而fragment可以在多个activity之间进行共享。
  • 可适应性:将UI的部分表示为不同的fragment,并根据屏幕的方向和尺寸使用不同的布局。

android_fragments_d001_why_fragments

在本教程中,你将构建一个暴走漫画的迷你百科全书。app将展示一个由暴走漫画构成的格子视图。当选中一副暴走漫画的时候,app就会展示与它相关的信息。你会从中学到:

  • 如何创建并添加fragment到activity上。
  • 如何让fragment发送信息到activity上。
  • 如何使用事务来添加或交换fragment。

注意 :本教程假定你已熟悉了Android编程的基础,并理解activity的生命周期的含义。还有几点值得去注意:

接下来就开始学习fragments!

Android Fragment入门

下载 初始项目 ,解压并启动 Android Studio 3.0 Beta 2 或更高的版本。

Welcome to Android Studio 对话框中,选择 Import project (Eclipse ADT, Gradle, etc.)

Android Studio Welcome Screen

选择初始项目的根目录,并点击 OK

Select project to import

如果看到了一个更新项目Gradle插件的信息,那是因为你使用了更高版本的Android Studio,选择“update”并继续。

查看项目,你会找到一些资源文件: strings.xml activity_main.xml drawable layout 。还有一些供你fragment使用的模板布局文件,非fragment的代码,以及一个fragment类,你可以在之后进行加工。

MainActivity 会持有你所有小小的fragment,而 RageComicListFragment 则包含了用来展示暴走漫画内容列表的代码,这样你就可以将注意力集中到fragment本身了。

运行项目。你会看到其中一无所有。
Running the starter app

很快你就会修复...

android_fragments_005_app_soon

Android Fragment的生命周期

和activity一样,fragment中也有着生命周期的概念,当fragment的状态发生变化的时候,就会触发相应的事件。例如,当fragment变为可见,活跃,无效,或被移除的状态时,一些事件就会发生。你可以在其中添加一些代码和行为来响应这些事件。

下面是 Android开发者文档 中,fragment生命周期的图表。 android_fragments_d002_fragment_lifecycle

android_fragments_006_fragment_lifecycle_hmm

当你添加一个fragment时,就会触发下列的生命周期事件:

  • onAttach :当fragment被依附到相应的activity上时被调用。
  • onCreate :当一个新的fragment实例被初始化时调用,通常发生在它被附加到相应activity上后被调用 - fragment有一点像是病毒。
  • onCreateView :当fragment创建了view层级中它自己的部分时,也就是被添加到了activity的view层级的时候被调用。
  • onActivityCreated :当fragment的activity完成了它的 onCreate 事件之后调用。
  • onStart :当fragment变为可见状态后调用。fragment只会在它的activity启动之后才会启动,且通常都是activity一启动之后,它就启动。
  • onResume :当fragment变为可见且可交互的状态后调用。fragment resume只会在它的activity resume之后,且通常都是activity一resume之后,它就resume。

但稍等,fragment还未完成。下列的生命周期的事件将在你移除一个fragment的时候发生:

  • onPause :当fragment变为不可交互的状态时触发。它仅会在一个fragment将被移除或替代的时候,或其activity被pause的时候发生。
  • onStop :当fragment变为不可见的状态时触发。它仅会在一个fragment将被移除或替代的时候,或其activity被停止的时候发生。
  • onDestroyView :当fragment的view和创建在 onCreateView 中的相关资源从activity的view层级中被移除并销毁的时候触发。
  • onDestroy :当fragment执行最后的清理时调用
  • onDetach :当fragment从所在的activity中移除的时候调用

正如你所看到的,fragment的生命周期始终伴随着activity的生命周期。但它还有一些相应于view层级,状态,附加/分离于activity的额外的事件。

v4支持库

在Android中,当使用fragment时,你可以使用两种fragment的实现。一种是由平台版本所提供的,也就是用户正在运行的Android的版本所提供。例如,一台运行Android 6.0(SDK版本23)的设备,就会运行库的平台版本23。

第二种是支持库的fragment。导入一个支持库到你的项目中和导入其它的第三方库没有什么差别。它在开发支持多版本Android的app时,有两个好处。

首先,它确保了你的代码在不同设备不同平台版本上的一致性。这就意味着bug的修复会在使用这些库的不同版本的Android上更加得一致。

其次,当新的特性被添加到最新版本的Android上时,Android的团队通常就会通过支持库来将其进行向后兼容,以便开发者使用较旧版本的Android。

到底该使用哪个库?

如果你要编写支持多个版本Android和多个版本设备的app,你就应当为Fragment采取支持库的方案。你同样需要为其它的功能采用支持库的方案。这是大多数的高级Android开发者和Android核心团队所认为的最佳实践。唯有在你想为非常特定版本的Android做开发的时候,才会采用平台库的方案。

换句话就是:如果可以的话,就尽量使用支持库的方案。对于fragment,就是使用 the v4 support library 了。

创建Fragment

最终,所有的暴走漫画会在启动的时候被展示位一个列表,点击其中的一项则会这幅漫画相应的详情。首先你将会创建详情页。

在Android Studio中打开初始项目,并在 app -> res -> layout 下找到 fragment_rage_comic_details.xml ,这个XML文件就指定了漫画详情展示的布局。它还展示了drawable资源和相关的string资源之一。

Fragment details preview

选择Android Studio的 Project tab 并找到 RageComicDetailsFragment 文件。这个类用来负责展示被选择漫画的展示详情。

RageComicDetailsFragment.kt 中,会看到类似如下的代码:

import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
//1
class RageComicDetailsFragment : Fragment() {
//2
companion object {
fun newInstance(): RageComicDetailsFragment {
return RageComicDetailsFragment()
}
}
//3
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.fragment_rage_comic_details, container, false)
}
}

上述代码:

  1. 声明 RageComicDetailsFragment 作为 Fragment 的子类。
  2. 提供了一个方法用来创建这个fragment的实例,这是一个工厂方法。
  3. 创建由fragment所控制的view的层级。

Activity使用 setContentView() 来指定它相应的布局文件,而fragment则在 onCreateView() 中创建它们的view层级。这里你调用 LayoutInflater.inflate 来创建 RageComicDetailsFragment 的层级。

inflate 的第三个参数确定了是否要将其添加到 container 上。container就是将持有fragment的view层级的父view。你应当总是将其设置为 false FragmentManager 将负责将fragment添加到container上。

这里有一个新的东东: FragmentManager 。每个activity都会有一个 FragmentManager 来管理它的fragment。它还提供了一个供你访问,添加和移除的界面。

你会注意到尽管 RageComicDetailsFragment 有一个工厂的实例方法 newInstance() ,但却没有任何的构造器。

为何要有一个工厂方法而不是构造器?

首先,由于你并没有定义任何的构造器,编译器就会自动生成一个空的,无参的默认构造器。这就是你需要有的构造器了,无需其它。

第二,你可能知道,当app进入后台的时候,Android会销毁并重新创建Activity及其所属的fragment。当activity再生的时候,它的 FragmentManager 就会使用默认的空构造器来重建fragment。如果无法找到,就会抛出一个异常。

因此,最后的实践就是永远都不要指定非空的构造器,实际上,最简单的做法就是你刚刚做的这样,不要指定构造器。

稍等,如果你需要将信息或数据传递给Fragment,该怎么做?跟上,你马上就将得到答案。

添加Fragment

添加你漂亮的fragment的最简单的办法,就是将它添加到XML的布局文件上。

打开 activity_main.xml ,选择Text tab,并添加下列的内容到根 FrameLayout 中:

<fragment
android:id="@+id/details_fragment"
class="com.raywenderlich.alltherages.RageComicDetailsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

这里在activity布局的内部放置了一个 <fragment> ,并指定了fragment的类型。 class 这个属性需要完整。 <fragment> 的view ID是 FragmentManager 所需求的。

运行项目,你就可以看到fragment了:

The details fragment in its full glory

动态地添加Fragment

首先,再次打开 activity_main.xml 并移除你刚放置的 <fragment> 。(是的,我知道你刚刚把它放到的这里 - 对不起。)你将使用暴走漫画的列表来替换它。

打开 RageComicListFragment.java ,它包含了所有可爱的列表代码。你可以看到 RageComicListFragment 没有显式的构造器,只有一个 newInstance()

RageComicListFragment 中的列表代码需要依赖于一些资源。你必须确保这个fragment对 Context 包含有效的引用。这就是 onAttach() 应该发挥作用的地方了。

打开 RageComicListFragment.kt ,并添加下列的import到已有import的下方:

import android.os.Bundle
import android.support.v7.widget.GridLayoutManager

GridLayoutManager ,用来在暴走漫画的列表中放置item。其它的import则是标准的fragment覆盖。

RageComicListFragment.kt 中,添加下列的两个方法,就在 RageComicAdapter 定义的上方:

  override fun onAttach(context: Context?) {
super.onAttach(context)
// Get rage face names and descriptions.
val resources = context!!.resources
names = resources.getStringArray(R.array.names)
descriptions = resources.getStringArray(R.array.descriptions)
urls = resources.getStringArray(R.array.urls)
// Get rage face images.
val typedArray = resources.obtainTypedArray(R.array.images)
val imageCount = names.size
imageResIds = IntArray(imageCount)
for (i in 0..imageCount - 1) {
imageResIds[i] = typedArray.getResourceId(i, 0)
}
typedArray.recycle()
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view: View = inflater!!.inflate(R.layout.fragment_rage_comic_list, container,
false)
val activity = activity
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view) as RecyclerView
recyclerView.layoutManager = GridLayoutManager(activity, 2)
recyclerView.adapter = RageComicAdapter(activity)
return view
}

onAttach() 中,访问了你需要通过 Context 来访问的资源。由于代码位于 onAttach() 中,你无需判断fragment是否包含一个有效的 Context

onCreateView() 中,你填充了 RageComicListFragment 的view的层级,它包含了一个 RecyclerView ,并执行了一些设置。

通常,如果你需要在fragment的view上进行一些处理, onCreateView() 就是一个很好的地方,因为这里已经能确定你的view准备好了。

接下来,打开 MainActivity.kt 并使用下列的代码替换 onCreate() 方法:

 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.root_layout, RageComicListFragment.newInstance(), "rageComicList")
.commit()
}
}

这里你将 RageComicListFragment 放置到 MainActivity 中。你会请求你的新朋友 FragmentManager 来添加它。

首先,你通过 supportFragmentManager 而非 fragmentManager 得到了 FragmentManager ,因为你使用的是支持框架。

然后通过调用 beginTransaction() 来请求 FragmentManager 开启一个新的事务 - 你应该可以单靠自己搞懂了。然后调用 add 来指定你想要的添加操作,并传递参数:

  • 用来在activity的布局中,容纳fragment的view层级的container的view ID。如果你偷偷看一眼 activity_main.xml ,你就会发现 @+id/root_layout
  • 待添加的fragment的实例。
  • 一个字符串,用来充当fragment实例的tag/identifier。这样便于 FragmentManager 在稍后检索fragment。

最后,你通过调用 commit() 来请求 FragmentManager 执行事务。

这样,fragment就被添加上了!

运行项目,你就会看到充满暴走漫画的列表:

The list of Rage Comics. Woo!

FragmentManager 通过 FragmentTransactions 实现功能,它们是fragment基本的操作,如添加,删除等等。

在上述的代码中, if 语句包含了展示fragment的代码,并判断activity尚无保存的状态。当activity被保存的时候,相应它所有fragment也会被保存。如果你不执行这这检查,就会发生这样的情况:

android_fragments_d003_fragments_too_many

你就会:

android_fragments_014_y_u_no_have

课程:牢记保存的状态会如何影响你的fragment。

数据绑定

环视项目,你会注意到一些事情:

  • 一个叫做 DataBindingAdapters 的文件。
  • 在app的模块 build.gradle 中,有一个 dataBinding 的引用:
    dataBinding {
    enabled = true
    }
  • recycler_item_rage_comic.xml 这个布局文件中的一个data的部分。
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable
    name="comic"
    type="com.raywenderlich.alltherages.Comic" />
    </data>
    ...
    </layout>
  • A Comic 的数据类。

如果你没有使用 数据绑定 ,可能就会像...

让我们来快速地看一遍。

通常,如果你想要设置在布局文件中的值,你就会在fragment和activity中使用类似如下的代码:

programmer.name = "a purr programmer"
view.findViewById<TextView>(R.id.name).setText(programmer.name)

问题是,如果你改变了 programmer name 的值,你也需要对 setText 做一个相应的 programmer 来更新此对象。想象一下,如果有一个工具,可以将你一个在fragment和activity中的变量和相应的view绑定起来,只要这个变量一改变,相应的view也就会发生变化。这就是 数据绑定 会为你做的事。

而在我们的app的 build.gradle 中,设置的 enabled=true 就打开了app的 数据绑定 。而数据类就包含了我们想在fragment中使用,并展示到view上的数据。 data 域中包含了 name type 选项,指定了被绑定变量的类型和名称。这个数据在view中使用了
{@} 符号。例如,下列的代码就将text域的值绑定到了 comic 变量的 name 字段上:

tools:text="@{comic.name}"

设置好view之后,你就需要访问你的view,并将变量 绑定到 它上面。这就是 数据绑定 的魔法将要出现的地方了!只要view有一个 data 字段,framework就会自动生成绑定的对象。通过将view的下划线方式的名称转换为驼峰式的名称,来推断对象的名称,并添加 绑定 到这个名称上。例如,一个被称作 recycler_item_rage_comic.xml 的view就会拥有一个被称作 RecyclerItemRageComicBinding 的绑定。

override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
//1
val recyclerItemRageComicBinding = RecyclerItemRageComicBinding.inflate(layoutInflater,
viewGroup, false)
//2
val comic = Comic(imageRmesIds[position], names[position], descriptions[position],
urls[position])
recyclerItemRageComicBinding.comic = comic

您可以通过在 绑定 对象上inflater的方法来填充view,并通过标准property访问机制来设置property。

数据绑定遵循了Model-View-ViewModel(MVVM)模式。MVVM包含三个组件:

  • A View :布局文件。
  • A Model :数据类
  • A View Model/Binder :自动生成的绑定文件。

关于MVVM及其它的设计模式,请访问教程: Common Design Patterns for Android 。你会看到更多关于数据绑定的内容。

与Activity进行通信

即使fragment被附加到了一个activity上,如果没有进一步的“鼓励”,它们就不一定要彼此交流。

对于所有的暴走漫画,你需要 RageComicListFragment 可以让 MainActivity 知道什么时候用户已作出了选择,这样 RageComicDetailsFragment 才可以将选择展示出来。

开始,打开 RageComicListFragment.kt 并添加下列的Java interface到文件的底部:

interface OnRageComicSelected {
fun onRageComicSelected(comic: Comic)
}

这就为activity定义了一个监听者的interface来监听fragment。activity将实现这个interface,而fragment就会在一项被选中时,调用 onRageComicSelected() ,将选择传递给activity。

RageComicListFragment 中,现有的字段下添加一个新的字段:

private lateinit var listener: OnRageComicSelected

这个字段会引用fragment的监听者,也就是activity。

onAttach() 方法中, super.onAttach(context); 之下,添加下列代码:

if (context is OnRageComicSelected) {
listener = context
} else {
throw ClassCastException(context.toString() + " must implement OnRageComicSelected.")
}

这里初始化了监听者的引用。在 onAttach() 中执行,可以确保fragment确实被附加到了activity上。然后你通过 instanceof 来验证相应的activity实现了 OnRageComicSelected interface。

如果判断失败,就抛出一个异常,因为你已无法继续下去了。反之,则将activity设置为 RageComicListFragment listener

onBindViewHolder() 方法中,添加下列的代码到它的底部 -- ok,我撒了一点小谎: RageComicAdapter 并没有包含你所需的 所有内容 !)

viewHolder.itemView.setOnClickListener { listener.onRageComicSelected(comic) }

这就添加了一个 View.OnClickListener 到每个暴走漫画上,以便它在listener(activity)上调用回调方法,来传递选择。

打开 MainActivity.java 并将类定义更新为如下的代码:

class MainActivity : AppCompatActivity(), RageComicListFragment.OnRageComicSelected {

这里会报出一个错误,让你将 MainActivity 设为abstract的,或实现abstract的方法 OnRageComicSelected(int, String, String, String) 。不要纠结这里,很快你就会解决它。

此代码指定了 MainActivity 会作为 OnRageComicSelected interface的一个实现。

现在,你将展示一个toast来验证代码是否可以正常地工作。添加下列的import到已有的import下面:

import android.widget.Toast

然后在 onCreate() 方法之后,添加下列的方法:

override fun onRageComicSelected(comic: Comic) {
Toast.makeText(this, "Hey, you selected " + comic.name + "!",
Toast.LENGTH_SHORT).show()
}

现在错误就消失了!运行项目,然后点击任一个暴走漫画,你就会看到一个被点击项目的toast消息:

You selected Neil deGrasse Tyson!

你已经让activity和它的fragment进行沟通了。你就像是一个数字外交官!

Fragment参数和事务

当前, RageComicDetailsFragment 展示了一个静态的 Drawable Strings 的集合,但你还想展示用户的选择。

首先,将在 fragment_rage_comic_details.xml 中全部的view替换为:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="comic"
type="com.raywenderlich.alltherages.Comic" />
</data>
<ScrollView xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:ignore="RtlHardcoded">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/name"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginTop="@dimen/rage_comic_name_margin_top"
android:text="@{comic.name}" />
<ImageView
android:id="@+id/comic_image"
android:layout_width="wrap_content"
android:layout_height="@dimen/rage_comic_image_size"
android:layout_marginBottom="@dimen/rage_comic_image_margin_vertical"
android:layout_marginTop="@dimen/rage_comic_image_margin_vertical"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:scaleType="centerCrop"
imageResource="@{comic.imageResId}" />
<TextView
android:id="@+id/description"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/rage_comic_description_margin_bottom"
android:layout_marginLeft="@dimen/rage_comic_description_margin_left"
android:layout_marginRight="@dimen/rage_comic_description_margin_right"
android:layout_marginTop="0dp"
android:autoLink="web"
android:text="@{comic.text}" />
</LinearLayout>
</ScrollView>
</layout>

我们在顶端为 Comic 添加了一个variable,将 name description 绑定到了 Comic 对象中同名的变量。

绑定Adapter

在暴走漫画的ImageView中,你会注意到如下的标签:

imageResource="@{comic.imageResId}"

这就对应于我们在 DataBindingAdapters.kt 文件中创建的绑定Adapter。

  @BindingAdapter("android:src")
fun setImageResoruce(imageView: ImageView, resource: Int) {
imageView.setImageResource(resource)
}

我们可以通过 binding adapter 在不被默认的 数据绑定 支持的元素上执行动作。这里为展示图片储存了一个resource的整型值,但数据绑定无法提供一个默认的方式来根据一个ID展示图片。要修复这个问题,你需要一个 BindingAdapter ,它引用了一个被调用的对象,及一个参数。用它来调用 imageView 上的 setImageResource 来展示暴走漫画。

现在view已经被设置好了,添加下列的import到 RageComicDetailsFragment.kt 文件的顶部:

import java.io.Serializable

newInstance() 替换为如下的代码:

private const val COMIC = "comic"
fun newInstance(comic: Comic): RageComicDetailsFragment {
val args = Bundle()
args.putSerializable(COMIC, comic as Serializable)
val fragment = RageComicDetailsFragment()
fragment.arguments = args
return fragment
}

fragment可以通过 arguments 这个fragment来获取初始化的参数。arguments实际上是一个用来储存键值对的 Bundle ,就像是在 Activity.onSaveInstanceState 中的 Bundle

你创建并填充了arguments的 Bundle ,并设置给 arguments ,当你在后面需要value的时候,你就引用 arguments property来获取它。

就像你在前面学到的,当fragment被重新创建的时候,会使用默认的空构造器 - 无需参数。

由于fragment可以从它的持久化参数中重新调用初始化参数,你就可以在重新创建的过程中使用它们。上述的代码还储存了在 RageComicDetailsFragment 参数中保存的被选中的暴走漫画的信息。

添加下列的import到 RageComicDetailsFragment.kt 文件顶部:

import com.raywenderlich.alltherages.databinding.FragmentRageComicDetailsBinding

onCreateView() 的内容替换为如下代码:

val fragmentRageComicDetailsBinding = FragmentRageComicDetailsBinding.inflate(inflater!!,
container, false)
val comic = arguments.getSerializable(COMIC) as Comic
fragmentRageComicDetailsBinding.comic = comic
comic.text = String.format(getString(R.string.description_format), comic.description, comic.url)
return fragmentRageComicDetailsBinding.root

由于你希望根据选择动态性地填充 Since you want to dynamically populate the UI of the RageComicDetailsFragment 的UI,因此首先在fragment的 onCreateView 中获取 FragmentRageComicDetailsBinding 的引用。然后,将你传给 RageComicDetailsFragment 的暴走漫画和相应的view进行绑定。

最后,在用户点击一项的时候,创建并展示一个 RageComicDetailsFragment ,替换仅仅展示toast。打开 MainActivity 并将 onRageComicSelected 中的逻辑替换为:

val detailsFragment =
RageComicDetailsFragment.newInstance(comic)
supportFragmentManager.beginTransaction()
.replace(R.id.root_layout, detailsFragment, "rageComicDetails")
.addToBackStack(null)
.commit()

你会发现,这里非常类似于之间将列表添加到 MainActivity 的代码,但也有一些值得注意的区别。

  • 创建一个包含一些漂亮参数的fragment的实例。
  • 调用 replace() 而不是 add ,来移除当前容器中的fragment,并添加新的Fragment。
  • 调用了另一个新朋友: FragmentTransaction 中的 addToBackStack() 。Fragment拥有 back栈 ,或是历史记录,就像Activity一样。

fragment的back栈并不独立于activity的back栈。它是宿主activity历史记录的一部分。

Fragments and back stack

当你在activity之间进行切换的时候,每个activity都会被放置在back栈上。每当你commit一个 FragmentTransaction 时,你就可以选择将它添加到back栈上。

那么, addToBackStack() 做了什么呢?它添加了 replace() 到back栈上,这样当用户点击返回按钮的时候,就可以进行撤销事务了。在本例中,点击返回按钮,用户就可以回到列表场景中。

列表的 add() 事务则会忽略调用 addToBackStack() 。这意味着事务是相同历史记录(整个activity)的一部分。如果用户在列表场景中点击返回按钮,就出跳出app。

现在,运行项目,当你点击其中一项的时候,你就可以看到该暴走漫画的详情了:

Yay! Actual details on Neil deGrasse Tyson

完工了!现在你就有了一个可以展示暴走漫画详情的 All The Rages app了。

从这儿去向哪里?

你可以从 这里 下载最终完成的项目。

关于fragment还有 很多 值得学习的地方。和任何一种工具或特性一样,考虑fragment是否适合你app的需求,如果是的话,尝试遵循最佳的实践和惯例。

为了让你的技能提升到一个新的台阶,以下有一些额外的事可以进行探索:

  • ViewPager 中使用fragment。很多app,包括Play Store,会通过 ViewPager 来使用一个可滑动,分页式的内容结构。
  • 使用更强大的 DialogFragment 来替换普通的“香草”对话框或 AlertDialog
  • 演练fragment如何与Activity的其它部分进行交互,例如app的工具栏。
  • 用fragment创建适应性的UI。实际上,你应当去运行 Adaptive UI in Android Tutorial
  • 使用fragment作为实现高级行为架构的一部分。你可以参考一下 Common Design Patterns for Android 来作为一个很好的起点,让架构搭建起来。